2. Measurements
2.1. Measurements for the heater/cooler shifter circuit
2.2. Measurements of temperature sensor circuit
3.The temperature control system
3.1. Controlling the PID
3.2. Testing the temperature control system
3.2.1. Holding the temperature constant
3.2.2. Change in temperature reference
3.2.3. Turning the lights on
3.2.4.Temperature stabilization over several days
A temperature sensor, a heater and a cooler are to be connected to a Arduino board. In order for it to work we need to have shifters for the voltage range to fit the inputs/outputs on the Arduino. This was first done by Vanessa Scheller using cables and a breadboard, I took her design and reworked it to be used on a milled circuit board and SMD parts.
The circuit for the temperature sensor looks as follows:
Fig. 1: temp. sensor shifter circuit
The input range from the temperature sensor we are interested in is $$-1.6V < U_{in} < 1.6V$$ The supply voltage of the op-amps is 15V. We want to have a cutoff after the first op-amp at the input range boundaries. This can be achieved with setting the amplification factor with $R_1$ and $R_2$. The output range should equal the input range of the Arduino analogue input which goes form 0-3.3V. The values found for the resistances are as follows (in $k\Omega$):
R1 = 15
R2 = 133
R3 = 133
R4 = 12 + 2.5 #5kOhm pot.
R5 = 10.0
R6 = 15 + 2.5 #5KOhm pot.
The formula for the output voltage with the voltage from REF03 being 2.5V:
def Uout(Uin):
if Uin < -1.6:
Uin = -1.6
if Uin > 1.6:
Uin = 1.6
return 2.5*R6*(R4+R3)/(R5+R6)/R3 + Uin*R2*R4/R1/R3
This gives to following theoretical response:
from fitting import FuncPlot
from plotly.offline import init_notebook_mode,iplot
import plotly.graph_objs as go
init_notebook_mode(connected=True)
plot = FuncPlot(Uout, -2, 2)
iplot(plot.getPlotData(title="Voltage curve", x_axis="U_in[V]", y_axis="U_out[V]"), show_link = False)
Graph 1: theoretical response
Fig. 2: heater/cooler shifter circuit
The output range from the Arduino's DAC is $$0.55V < U_{DAC} < 2.75V$$ The regulation voltage from the heater and cooler is 0 to 10V. First the voltage from the DAC out needs to be reduced by 0.55V and then amplified such that the maximum out from the DAC corresponds to the maximum in from the heater/cooler. The amplification is set with the resistance RG. The values found by Vanessa are (in $k\Omega$):
Ra = 3.92 + 2.5 #5kOhm pot
Rb = 1.0
RG = 27 + 5 #10kOhm pot.
The output here can be described by the amplified difference of the two input voltages. The amplification factor was adjusted such that it gives the needed output range.
To both circuits many capacitances are added, which are only for filtering the input +15V/-15V and the ref03 voltages from AC parts. This is done with 10nF and 100nF capacitances in parallel.
All of the circuits were realized using a smd board. The circuitboard was constructed using EAGLE Professional and made with a circuit board cutter. It has the same proportions as the used arduino due, such that it fits into the enclosure thats already existing.
Fig. 3: Circuitboard layout
The resistors and the capacitances are directly soldered onto the board, while the OP07 and the AD622 (not 620 as on the image) are put onto headers. The complete board is shown here:
Fig. 4: Circuitboard picture
Notice that the header for the left AD622 was placed in the opposite direction, such that the marker for the top is actually on the other side than the top is supposed to be.
Also some of the capacitances were not soldered in, on the design of the board they were made as a precaution.
It was found that the heater/cooler cables add a capacitance which lead to oscillations on the temperature measurement. We added $100 \Omega$ resistors on the heater/cooler output to resolve the issue.
Below is a picture of the board connected to the Arduino with the Ethernet Shield 2 on top.
Fig.5: Board connected to Arduino
After everything on the board is done I put everything into the housing in the cabinet.
Fig. 6: Board and Arduino in the housing
We measure the output voltage from the heater and cooler shifter while changing the input through the Arduino DAC outputs. The DAC outputs use 8Bit resolution, so we measure from 0 to 255 Bits.
import numpy as np
x = np.array([ 0, 25, 50, 75, 100, 125, 150, 175, 200, 225, 250, 255])
y_oben = np.array([0.011, 0.997, 1.983, 2.969, 3.955, 4.941, 5.929, 6.91, 7.90, 8.89, 9.87, 10.07])
y_unten = np.array([0.002, 0.982, 1.961, 2.942, 3.921, 4.902, 5.883, 6.86, 7.84, 8.82, 9.80, 10.00])
from fitting import Fit
y_error_oben = np.vectorize((lambda x: 0.01 + 0.01*x if x > 6.0 else 0.001 + 0.01*x))(y_oben)
y_error_unten = np.vectorize((lambda x: 0.01 + 0.01*x if x > 6.0 else 0.001 + 0.01*x))(y_oben)
#linear fit function
def linear_fit(x,m,c):
return m*x+c
oben = Fit(x, y_oben,y_error_oben)
oben.curvefit(linear_fit)
iplot(oben.getPlotData(title="Top shifter", x_axis="Bits", y_axis="U_out[V]"), show_link = False)
unten = Fit(x, y_unten,y_error_unten)
unten.curvefit(linear_fit)
iplot(unten.getPlotData(title="Bottom shifter", x_axis="Bits", y_axis="U_out[V]"), show_link = False)
Graph 2: heater/cooler shifter
It's visible that the components act as expected, i.e. linearity is achieved. The minimum out voltages are near enough to 0 and the maximum voltages are close enough to 10V.
We first measure the voltages (and set them with the variable resistances) on the output for the min/max values -1.6V/1.6V and make sure that above/below these voltages the output voltage does not change anymore. Then we measure the values inbetween.
Uin = np.array([ -5.0, 2.5, -1.9, -1.6, -1.4, -1.2, -1.0, -0.8, -0.6, -0.4 , -0.2, 0.0, 0.2, 0.4, 0.6, 0.8, 1.0, 1.2, 1.4, 1.6, 1.9, 2.5, 5.0])
Uout = np.array([0.001, 0.000, 0.000, 0.009, 0.219, 0.428, 0.637, 0.846, 1.056, 1.265, 1.474, 1.684, 1.894, 2.103, 2.312, 2.522, 2.731, 2.941, 3.150, 3.246, 3.244, 3.246, 3.245])
Uout_err = (lambda x: 0.01*x + 0.001)(Uout)
temp = Fit(Uin,Uout,Uout_err)
temp.curvefit(linear_fit, bounds = (3,-4))
iplot(temp.getPlotData(title="temp shifter", x_axis="U_in[V]", y_axis="U_out[V]"), show_link = False)
Graph 3: response of the temp. shifter
We see that we have linearity between -1.6V and 1.6V (slight drop before 1.6V) and afterwards the desired plateaus.
The temperature value gets read out on a arduino due platform. It then checks with the temperature reference and then calculates control values for the heater / cooler with a PID controller, which was realized as a software running on the arduino. This was originally developed by Vanessa Scheller. I worked with her
log book and report.
One of my tasks was to install the Ethernet Shield 2 on the Arduino in order for the PID controller to be abled to communicate more easily with a computer. The existing possibility to communicate with the Arduino was to do it over the usb serial interface of the Arduino which allows to read out data and also send data to the Arduino, but requires a computer to be connected to the Arduino at all times. The advantage of a network interface is that it just needs a connected ethernet cable to the Arduino.
The Ethernet Shield 2 is just connected to the Arduino like any other Arduino shield, by just plugging it on top of the Arduino. The Arduino acts as a server which communicates with a client program running on the tritium-vserver.
The client sends commands directly to the Arduino which get handled there in a defined time intervall. The Arduino sends back a response including information.
The client program runs in the background and issues the command to receive the serial output from the arduino and stores those directly in a file as is, but also creates a THee file with only the time and the temperature values.
The client program can be controlled with the controller script. Running the script with a command line argument makes the client program send the argument as a command to the Arduino. The answer from the arduino is stored in a log file and also printed on the console.
Getting the serial output
Send the command g to receive the serial output. The values received are:
Getting the PID parameters and the temperature set value
Send the command gpid to receive the values $K_P$, $T_N$ and $T_V$ for both the heater and the cooler as well as the current temperature set value (which is titled sollwert). Also get the state of the PID (1 for turned on, 0 for turned off).
Setting the PID parameters and setting fixed heater/cooler set values
Send the command xy###, where x is either h or c for heater/cooler and y is either p,i,d for the respective part of the regulator. y can also be f for a fixed value of the heater/cooler set value (this turns of the PID control). ### corresponds to the wanted value (again in 8 bits from 0 to 255). The Arduino programm checks whether the value is in bounds.
Setting the temperature reference
Send the command so### to set the temperature reference to the value ### (in 10 bits).
Turning PID off and back on
Send the command pidon / pidoff to turn the PID on or off.
The calibration for the temperature values of the sensor is as follows:
def calib(bit):
return 0.0253714*bit + 12.0968
plot = FuncPlot(calib, 0, 1023)
iplot(plot.getPlotData(title="Temperature calibration", x_axis="Bit", y_axis="Temp [°C]",
fitres = 1024), show_link = False)
Graph 4: Temperature calibration
The board is now connected to the arduino, the temperature sensor and the heater/cooler. Now I measure for a constant temperature reference the temperature with already existing sensors (i.e. top window, top door and the pid temperature sensor).
We want to see now, how well the temperature stabilization works for keeping the temperature constant over the day (18.02.2017). We get the data for one day from the THee-Files located in the tritium-drive (the pid-client program also produces Thee-files there).
Also a histogram of the data at the PID-sensor is plotted.
from THeePlot import THeePlot
calib_pid = lambda y: 0.0253714*y + 12.0968
plot1 = THeePlot("./../data/T_PID_2017-02-18.THee", calib_pid, 1)
iplot(plot1.getPlotData(title="T_PID", x_axis="Time [h]", y_axis="Temp [°C]",connected = True), show_link = False)
calib_door = lambda y: -0.001827*y + 27.346001
plot2 = THeePlot("./../data/T_Top_Door_2017-02-18.THee", calib_door, 10)
iplot(plot2.getPlotData(title="T_Top_Door", x_axis="Time [h]", y_axis="Temp [°C]",connected = True), show_link = False)
calib_window = lambda y: -0.001821*y + 27.933001
plot3 = THeePlot("./../data/T_Top_Window_2017-02-18.THee", calib_window, 10)
iplot(plot3.getPlotData(title="T_Top_Window", x_axis="Time [h]", y_axis="Temp [°C]",connected = True), show_link = False)
from fitting import Hist
yval = np.array(plot1.getPlotData()['data'][0]['y'])
plot4 = Hist(yval, binsize = 0.01)
iplot(plot4.getPlotData(), show_link = False)
print("Mean = {:.3f}".format(np.mean(yval)) + "K , " + " std = {:.3f}".format(np.mean(std)) + "K")
Graph 5: Temperature stabilization over one day with temp. from sensors T_PID, T_Top_Door, T_Top_Window
We see that for the sensor the PID uses to regulate we have a good nearly constant temperature (fluctuations $\sim 0.5 °C$ and $\sigma = 0.057K$). The other two sensor show a fast rise(window) and a fall(door) in temperature at around 12 o'clock. I can now look at the cooler/heater values and see that the cooler was shut off up until nearly 12 o'clock and then turned on (probably due to rise in outside temperature), while the heater value decreased.
The heater is located closer to the window and the cooler closer to the door. The rise in outside temperature could be due to sun coming in the windows thus raising the temperature there fast. The drop at the door should then be due to the cooler being turned on.
To see how the system reacts to rapid changes in temperature it is much easier to look at a change in the reference temperature. For this we change the value of the reference temperature from 450 Bits ($\sim 23.5 °C$) to 500 Bits ($\sim 24.8 °C$).
Again we look at the temperature values from the 3 sensors.
calib_pid = lambda y: 0.0253714*y + 12.0968
plot1 = THeePlot("./../data/T_PID_2017-02-20.THee", calib_pid, 1)
iplot(plot1.getPlotData(title="T_PID", x_axis="Time [h]", y_axis="Temp [°C]",connected = True,
x_range = [8,16]), show_link = False)
calib_door = lambda y: -0.001827*y + 27.346001
plot2 = THeePlot("./../data/T_Top_Door_2017-02-20.THee", calib_door, 10)
iplot(plot2.getPlotData(title="T_Top_Door", x_axis="Time [h]", y_axis="Temp [°C]",connected = True,
x_range = [8,16]), show_link = False)
calib_window = lambda y: -0.001821*y + 27.933001
plot3 = THeePlot("./../data/T_Top_Window_2017-02-20.THee", calib_window, 10)
iplot(plot3.getPlotData(title="T_Top_Window", x_axis="Time [h]", y_axis="Temp [°C]",connected = True,
x_range = [8,16]), show_link = False)
Graph 6: response to temperature reference change on the different sensor
We can see that the temperature at the door and at the PID sensor react very similarly, the temperature at the window seems to react a little slower.
We can now compute the relaxation time of the system with the following fit:
f = open("./../data/T_PID_2017-02-20.THee")
f.readline(); f.readline(); f.readline()
x,y = [],[]
for line in f:
a,b = line.split()
x.append(float(a)/60)
y.append(calib_pid(float(b)))
y_err = 0.1*np.ones(len(x) + 1)
test = Fit(np.array(x),np.array(y),y_err)
def expup(t, A, B, tau):
return B + A*(1-np.exp(-(t-675)/tau))
test.curvefit(expup, output = True, p0 = [1,23,60], bounds = (675,806))
def expdown(t, A, B, tau):
return B + A*np.exp(-(t-807)/tau)
test.curvefit(expdown, output = True, p0 = [1,23,30], bounds = (807,960))
iplot(test.getPlotData(title="T_PID", x_axis="Time [min]", y_axis="Temp [°C]", connected = True,
disp_error = False, x_range = [10*60,16.2*60], fitres = 500), show_link = False)
Graph 7: Response of the pid-controller for a change of 50 bits in temperature
The time constants are:
It is also visible that the temperature does not overshoot and does not strongly oscillate.
I do another measurement to confirm the behaviour. This one is done at night (Temp to 500 Bits at 19:11 and back to 450 Bits at 22:45).
plot1 = THeePlot("./../data/PID_2017-02-21-22.THee", calib_pid, 1)
iplot(plot1.getPlotData(title="T_PID", x_axis="Time [h]", y_axis="Temp [°C]",connected = True,
x_range = [18,27]), show_link = False)
plot2 = THeePlot("./../data/Door_2017-02-21-22.THee", calib_door, 10)
iplot(plot2.getPlotData(title="T_Top_Door", x_axis="Time [h]", y_axis="Temp [°C]",connected = True,
x_range = [18,27]), show_link = False)
plot3 = THeePlot("./../data/Window_2017-02-21-22.THee", calib_window, 10)
iplot(plot3.getPlotData(title="T_Top_Window", x_axis="Time [h]", y_axis="Temp [°C]",connected = True,
x_range = [18,27]), show_link = False)
Graph 8: Second measurement of temperature under change of reference temperature
f = open("./../data/PID_2017-02-21-22.THee")
f.readline(); f.readline(); f.readline()
x,y = [],[]
for line in f:
a,b = line.split()
x.append(float(a)/60)
y.append(calib_pid(float(b)))
y_err = 0.1*np.ones(len(x) + 1)
test = Fit(np.array(x),np.array(y),y_err)
def expup(t, A, B, tau):
return B + A*(1-np.exp(-(t-1152)/tau))
test.curvefit(expup, output = True, p0 = [2,23,60], bounds = (1062,1275))
def expdown(t, A, B, tau):
return B + A*np.exp(-(t-1365)/tau)
test.curvefit(expdown, output = True, p0 = [1,23,30], bounds = (1276,1550))
iplot(test.getPlotData(title="T_PID", x_axis="Time [min]", y_axis="Temp [°C]", connected = True,
disp_error = False, x_range = [1055,2000], fitres = 500), show_link = False)
Graph 9: Step response 2 of PID on PID temp. sensor
We get a similar value for the upwards slope. The downwards slope is slightly faster than before, which might be cause by lower outside temperature. We see here that some time after the temperature reaches the lower value again it starts to oscillate heavier.
This is also visible for the Door temperature, which we can also fit.
f = open("./../data/Door_2017-02-21-22.THee")
f.readline(); f.readline(); f.readline()
x,y = [],[]
i = 60
for line in f:
if i == 60:
a,b = line.split()
x.append(float(a)/60)
y.append(calib_door(float(b)))
i = 0
i += 1
y_err = 0.1*np.ones(len(x) + 1)
test = Fit(np.array(x),np.array(y),y_err)
def expup(t, A, B, tau):
return B + A*(1-np.exp(-(t-1152)/tau))
test.curvefit(expup, output = True, p0 = [2,23,60], bounds = (47,260))
def expdown(t, A, B, tau):
return B + A*np.exp(-(t-1365)/tau)
test.curvefit(expdown, output = True, p0 = [1,23,30], bounds = (261,600))
iplot(test.getPlotData(title="T_Top_Door", x_axis="Time [min]", y_axis="Temp [°C]", connected = True,
disp_error = False, x_range = [1055,2000], fitres = 500), show_link = False)
Graph 10: Step response 2 on Top_Door temperature sensor
Here we get nearly the same upwards time constant, but a lower downwards time constant.
At 11 o'clock I turn the lights on in the control room. The reaction at the different sensors is shown below, also the set values of the heater/cooler are shown.
plot1 = THeePlot("./../data/T_PID_2017-02-22.THee", calib_pid, 1)
iplot(plot1.getPlotData(title="T_PID", x_axis="Time [h]", y_axis="Temp [°C]",connected = True,
x_range = [7,14]), show_link = False)
plot2 = THeePlot("./../data/T_Top_Door_2017-02-22.THee", calib_door, 10)
iplot(plot2.getPlotData(title="T_Top_Door", x_axis="Time [h]", y_axis="Temp [°C]",connected = True,
x_range = [7,14]), show_link = False)
plot3 = THeePlot("./../data/T_Top_Window_2017-02-22.THee", calib_window, 10)
iplot(plot3.getPlotData(title="T_Top_Window", x_axis="Time [h]", y_axis="Temp [°C]",connected = True,
x_range = [7,14]), show_link = False)
Graph 11: Temperature response to light being turned on
f = open("./../data/light.dat")
x,y1,y2 = [],[],[]
for line in f:
a,b,c = line.strip().split()
x.append(int(a)/60/60); y1.append(float(b)); y2.append(float(c))
plot1 = Fit(x,y1)
plot2 = Fit(x,y2)
iplot(plot1.getPlotData(title="Heater", x_axis="Time [h]", y_axis="Set value [Bits]",connected = True), show_link = False)
iplot(plot2.getPlotData(title="Cooler", x_axis="Time [h]", y_axis="Set value [Bits]",connected = True), show_link = False)
Graph 12: Heater/cooler set values after light being turned on
The first 3 plots show that the temperature does oscillate more after turning the lights on, especially more to the higher temperatures.
Below we see that while the heater set value nearly stays the same the cooler starts cooling more after turning the lights on.
Over the weekend of the 24th to 27th I could make a measurement over two and a half days. The measurement starts on the 24th at around 16:30 and ends at the 27th at around 4:30. Here I can also do a histogram for the door and window temperatures, as there is no big jump in them.
plot1 = THeePlot("./../data/T_PID_2017-02-24-27.THee", calib_pid, 2)
iplot(plot1.getPlotData(title="T_PID", x_axis="Time [h]", y_axis="Temp [°C]",connected = True,
x_range = [16,77]), show_link = False)
plot2 = THeePlot("./../data/T_Top_Door_2017-02-24-27.THee", calib_door, 120)
iplot(plot2.getPlotData(title="T_Top_Door", x_axis="Time [h]", y_axis="Temp [°C]",connected = True,
x_range = [16,77]), show_link = False)
plot3 = THeePlot("./../data/T_Top_Window_2017-02-24-27.THee", calib_window, 120)
iplot(plot3.getPlotData(title="T_Top_Window", x_axis="Time [h]", y_axis="Temp [°C]",connected = True,
x_range = [16,77]), show_link = False)
yval1 = np.array(THeePlot("./../data/T_PID_2017-02-24-27.THee", calib_pid, 1).getPlotData()['data'][0]['y'])[480:-1140]
plot4 = Hist(yval1, binsize = 0.01)
iplot(plot4.getPlotData(title="T_PID Histogram", x_axis="Temperature", y_axis="Count"), show_link = False)
print("Mean = {:.3f}".format(np.mean(yval1)) + "K , " + " Std = {:.3f}".format(np.std(yval1)) + "K")
yval2 = np.array(THeePlot("./../data/T_Top_Door_2017-02-24-27.THee", calib_door, 1).getPlotData()['data'][0]['y'])
plot5 = Hist(yval2, binsize = 0.01)
iplot(plot5.getPlotData(title="T_Top_Door Histogram", x_axis="Temperature", y_axis="Count"), show_link = False)
print("Mean = {:.3f}".format(np.mean(yval2)) + "K , " + " Std = {:.3f}".format(np.std(yval2)) + "K")
yval3 = np.array(THeePlot("./../data/T_Top_Window_2017-02-24-27.THee", calib_window, 1).getPlotData()['data'][0]['y'])
plot6 = Hist(yval3, binsize = 0.02)
iplot(plot6.getPlotData(title="T_Top_Window Histogram", x_axis="Temperature", y_axis="Count"), show_link = False)
print("Mean = {:.3f}".format(np.mean(yval3)) + "K , " + " Std = {:.3f}".format(np.std(yval3)) + "K")